/*
 * Copyright (c) 2007, 2015, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
/*
 * Copyright 1999-2002,2004 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.sun.org.apache.xerces.internal.dom;

import org.w3c.dom.DOMException;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * CharacterData is an abstract Node that can carry character data as its
 * Value.  It provides shared behavior for Text, CData, and
 * possibly other node types. All offsets are 0-based.
 * <p>
 * Since ProcessingInstructionImpl inherits from this class to reuse the
 * setNodeValue method, this class isn't declared as implementing the interface
 * CharacterData. This is done by relevant subclasses (TexImpl, CommentImpl).
 * <p>
 * This class doesn't directly support mutation events, however, it notifies
 * the document when mutations are performed so that the document class do so.
 *
 * @xerces.internal
 * @since PR-DOM-Level-1-19980818.
 */
public abstract class CharacterDataImpl
    extends ChildNode {

  //
  // Constants
  //

  /**
   * Serialization version.
   */
  static final long serialVersionUID = 7931170150428474230L;

  //
  // Data
  //

  protected String data;

  /**
   * Empty child nodes.
   */
  private static transient NodeList singletonNodeList = new NodeList() {
    public Node item(int index) {
      return null;
    }

    public int getLength() {
      return 0;
    }
  };

  //
  // Constructors
  //

  public CharacterDataImpl() {
  }

  /**
   * Factory constructor.
   */
  protected CharacterDataImpl(CoreDocumentImpl ownerDocument, String data) {
    super(ownerDocument);
    this.data = data;
  }

  //
  // Node methods
  //

  /**
   * Returns an empty node list.
   */
  public NodeList getChildNodes() {
    return singletonNodeList;
  }

  /*
   * returns the content of this node
   */
  public String getNodeValue() {
    if (needsSyncData()) {
      synchronizeData();
    }
    return data;
  }

  /**
   * Convenience wrapper for calling setNodeValueInternal when
   * we are not performing a replacement operation
   */
  protected void setNodeValueInternal(String value) {
    setNodeValueInternal(value, false);
  }

  /**
   * This function added so that we can distinguish whether
   * setNodeValue has been called from some other DOM functions.
   * or by the client.<p>
   * This is important, because we do one type of Range fix-up,
   * from the high-level functions in CharacterData, and another
   * type if the client simply calls setNodeValue(value).
   */
  protected void setNodeValueInternal(String value, boolean replace) {

    CoreDocumentImpl ownerDocument = ownerDocument();

    if (ownerDocument.errorChecking && isReadOnly()) {
      String msg = DOMMessageFormatter
          .formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null);
      throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, msg);
    }

    // revisit: may want to set the value in ownerDocument.
    // Default behavior, overridden in some subclasses
    if (needsSyncData()) {
      synchronizeData();
    }

    // keep old value for document notification
    String oldvalue = this.data;

    // notify document
    ownerDocument.modifyingCharacterData(this, replace);

    this.data = value;

    // notify document
    ownerDocument.modifiedCharacterData(this, oldvalue, value, replace);
  }

  /**
   * Sets the content, possibly firing related events,
   * and updating ranges (via notification to the document)
   */
  public void setNodeValue(String value) {

    setNodeValueInternal(value);

    // notify document
    ownerDocument().replacedText(this);
  }

  //
  // CharacterData methods
  //

  /**
   * Retrieve character data currently stored in this node.
   *
   * @throws DOMExcpetion(DOMSTRING_SIZE_ERR) In some implementations, the stored data may exceed
   * the permitted length of strings. If so, getData() will throw this DOMException advising the
   * user to instead retrieve the data in chunks via the substring() operation.
   */
  public String getData() {
    if (needsSyncData()) {
      synchronizeData();
    }
    return data;
  }

  /**
   * Report number of characters currently stored in this node's
   * data. It may be 0, meaning that the value is an empty string.
   */
  public int getLength() {
    if (needsSyncData()) {
      synchronizeData();
    }
    return data.length();
  }

  /**
   * Concatenate additional characters onto the end of the data
   * stored in this node. Note that this, and insert(), are the paths
   * by which a DOM could wind up accumulating more data than the
   * language's strings can easily handle. (See above discussion.)
   *
   * @throws DOMException(NO_MODIFICATION_ALLOWED_ERR) if node is readonly.
   */
  public void appendData(String data) {

    if (isReadOnly()) {
      String msg = DOMMessageFormatter
          .formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null);
      throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, msg);
    }
    if (data == null) {
      return;
    }
    if (needsSyncData()) {
      synchronizeData();
    }

    setNodeValue(this.data + data);

  } // appendData(String)

  /**
   * Remove a range of characters from the node's value. Throws a
   * DOMException if the offset is beyond the end of the
   * string. However, a deletion _count_ that exceeds the available
   * data is accepted as a delete-to-end request.
   *
   * @throws DOMException(INDEX_SIZE_ERR) if offset is negative or greater than length, or if count
   * is negative.
   * @throws DOMException(NO_MODIFICATION_ALLOWED_ERR) if node is readonly.
   */
  public void deleteData(int offset, int count)
      throws DOMException {

    internalDeleteData(offset, count, false);
  } // deleteData(int,int)


  /**
   * NON-DOM INTERNAL: Within DOM actions, we sometimes need to be able
   * to control which mutation events are spawned. This version of the
   * deleteData operation allows us to do so. It is not intended
   * for use by application programs.
   */
  void internalDeleteData(int offset, int count, boolean replace)
      throws DOMException {

    CoreDocumentImpl ownerDocument = ownerDocument();
    if (ownerDocument.errorChecking) {
      if (isReadOnly()) {
        String msg = DOMMessageFormatter
            .formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null);
        throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, msg);
      }

      if (count < 0) {
        String msg = DOMMessageFormatter
            .formatMessage(DOMMessageFormatter.DOM_DOMAIN, "INDEX_SIZE_ERR", null);
        throw new DOMException(DOMException.INDEX_SIZE_ERR, msg);
      }
    }

    if (needsSyncData()) {
      synchronizeData();
    }
    int tailLength = Math.max(data.length() - count - offset, 0);
    try {
      String value = data.substring(0, offset) +
          (tailLength > 0 ? data.substring(offset + count, offset + count + tailLength) : "");

      setNodeValueInternal(value, replace);

      // notify document
      ownerDocument.deletedText(this, offset, count);
    } catch (StringIndexOutOfBoundsException e) {
      String msg = DOMMessageFormatter
          .formatMessage(DOMMessageFormatter.DOM_DOMAIN, "INDEX_SIZE_ERR", null);
      throw new DOMException(DOMException.INDEX_SIZE_ERR, msg);
    }

  } // internalDeleteData(int,int,boolean)

  /**
   * Insert additional characters into the data stored in this node,
   * at the offset specified.
   *
   * @throws DOMException(INDEX_SIZE_ERR) if offset is negative or greater than length.
   * @throws DOMException(NO_MODIFICATION_ALLOWED_ERR) if node is readonly.
   */
  public void insertData(int offset, String data)
      throws DOMException {

    internalInsertData(offset, data, false);

  } // insertData(int,int)


  /**
   * NON-DOM INTERNAL: Within DOM actions, we sometimes need to be able
   * to control which mutation events are spawned. This version of the
   * insertData operation allows us to do so. It is not intended
   * for use by application programs.
   */
  void internalInsertData(int offset, String data, boolean replace)
      throws DOMException {

    CoreDocumentImpl ownerDocument = ownerDocument();

    if (ownerDocument.errorChecking && isReadOnly()) {
      String msg = DOMMessageFormatter
          .formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null);
      throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, msg);
    }

    if (needsSyncData()) {
      synchronizeData();
    }
    try {
      String value =
          new StringBuffer(this.data).insert(offset, data).toString();

      setNodeValueInternal(value, replace);

      // notify document
      ownerDocument.insertedText(this, offset, data.length());
    } catch (StringIndexOutOfBoundsException e) {
      String msg = DOMMessageFormatter
          .formatMessage(DOMMessageFormatter.DOM_DOMAIN, "INDEX_SIZE_ERR", null);
      throw new DOMException(DOMException.INDEX_SIZE_ERR, msg);
    }

  } // internalInsertData(int,String,boolean)


  /**
   * Replace a series of characters at the specified (zero-based)
   * offset with a new string, NOT necessarily of the same
   * length. Convenience method, equivalent to a delete followed by an
   * insert. Throws a DOMException if the specified offset is beyond
   * the end of the existing data.
   *
   * @param offset The offset at which to begin replacing.
   * @param count The number of characters to remove, interpreted as in the delete() method.
   * @param data The new string to be inserted at offset in place of the removed data. Note that the
   * entire string will be inserted -- the count parameter does not affect insertion, and the new
   * data may be longer or shorter than the substring it replaces.
   * @throws DOMException(INDEX_SIZE_ERR) if offset is negative or greater than length, or if count
   * is negative.
   * @throws DOMException(NO_MODIFICATION_ALLOWED_ERR) if node is readonly.
   */
  public void replaceData(int offset, int count, String data)
      throws DOMException {

    CoreDocumentImpl ownerDocument = ownerDocument();

    // The read-only check is done by deleteData()
    // ***** This could be more efficient w/r/t Mutation Events,
    // specifically by aggregating DOMAttrModified and
    // DOMSubtreeModified. But mutation events are
    // underspecified; I don't feel compelled
    // to deal with it right now.
    if (ownerDocument.errorChecking && isReadOnly()) {
      String msg = DOMMessageFormatter
          .formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null);
      throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, msg);
    }

    if (needsSyncData()) {
      synchronizeData();
    }

    //notify document
    ownerDocument.replacingData(this);

    // keep old value for document notification
    String oldvalue = this.data;

    internalDeleteData(offset, count, true);
    internalInsertData(offset, data, true);

    ownerDocument.replacedCharacterData(this, oldvalue, this.data);

  } // replaceData(int,int,String)

  /**
   * Store character data into this node.
   *
   * @throws DOMException(NO_MODIFICATION_ALLOWED_ERR) if node is readonly.
   */
  public void setData(String value)
      throws DOMException {
    setNodeValue(value);
  }

  /**
   * Substring is more than a convenience function. In some
   * implementations of the DOM, where the stored data may exceed the
   * length that can be returned in a single string, the only way to
   * read it all is to extract it in chunks via this method.
   *
   * @param offset Zero-based offset of first character to retrieve.
   * @param count Number of characters to retrieve.
   *
   * If the sum of offset and count exceeds the length, all characters to end of data are returned.
   * @throws DOMException(INDEX_SIZE_ERR) if offset is negative or greater than length, or if count
   * is negative.
   * @throws DOMException(WSTRING_SIZE_ERR) In some implementations, count may exceed the permitted
   * length of strings. If so, substring() will throw this DOMException advising the user to instead
   * retrieve the data in smaller chunks.
   */
  public String substringData(int offset, int count)
      throws DOMException {

    if (needsSyncData()) {
      synchronizeData();
    }

    int length = data.length();
    if (count < 0 || offset < 0 || offset > length - 1) {
      String msg = DOMMessageFormatter
          .formatMessage(DOMMessageFormatter.DOM_DOMAIN, "INDEX_SIZE_ERR", null);
      throw new DOMException(DOMException.INDEX_SIZE_ERR, msg);
    }

    int tailIndex = Math.min(offset + count, length);

    return data.substring(offset, tailIndex);

  } // substringData(int,int):String

} // class CharacterDataImpl
